// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "hue_range.h"
#include <limits>
#include <algorithm>
#include <cmath>

using std::numeric_limits;
using max_flow::SimpleMaxFlow;
using max_flow::NodeIndex;
using max_flow::ArcIndex;
using max_flow::FlowQuantity;

namespace harmonizer {

HueRange::HueRange(float from, float to) : split_occurred_(false) {
  Reset(from, to);
}

HueRange::HueRange(float from, float to, bool split_occurred) :
    split_occurred_(split_occurred) {
  Reset(from, to);
}

HueRange::HueRange(const std::vector<float>& vector_of_ranges) :
    split_occurred_(false) {
  bool retval = Reset(vector_of_ranges);
  CHECK(retval);
}

HueRange::HueRange(const std::vector<float>& vector_of_ranges,
                   bool split_occurred) : split_occurred_(split_occurred) {
  bool retval = Reset(vector_of_ranges);
  CHECK(retval);
}

HueRange::~HueRange() {
  // Nothing to do here
}

void HueRange::Reset(float from, float to) {
  hue_segments_.clear();
  hue_segments_.push_back(Segment(from, to));
}

bool HueRange::Reset(const std::vector<float>& vector_of_ranges) {
  if (0 != (vector_of_ranges.size() % 2)) {
    return false;
  }

  hue_segments_.clear();

  for (int i = 0; i < vector_of_ranges.size(); i += 2) {
    HueRange::Segment current_segment(vector_of_ranges[i],
                                      vector_of_ranges[i+1]);

    // Make sure the segment we are about to add does not overlap previous
    // segments
    if (IsOverlappingRange(current_segment)) {
      return false;
    }

    hue_segments_.push_back(current_segment);
  }

  return true;
}

bool HueRange::IsValueWithinRange(float hue_value) const {
  for (Segment current_segment : hue_segments_) {
    if (current_segment.IsWithinSegment(hue_value)) {
      return true;
    }
  }

  return false;
}

bool HueRange::IsOverlappingRange(Segment segment) const {
  for (Segment current_segment : hue_segments_) {
    if (current_segment.IsOverlappingSegment(segment)) {
      return true;
    }
  }

  return false;
}

bool HueRange::IsOverlappingRange(float from, float to) const {
  return IsOverlappingRange(Segment(from, to));
}

int HueRange::GetClosestSegmentIndices(float hue_value) const {
  float min_distance = std::numeric_limits<float>::max();
  int closest_ind = -1;

  for (int i = 0; i < hue_segments_.size(); ++i) {
    // TODO: explore other options here, such as measuring distance to
    // the center of the segment, or actual distance the hue will be shifted
    // (given some given fit method).
    float current_distance = hue_segments_[i].DistanceFromEdge(hue_value);

    if (current_distance <= min_distance) {
      min_distance = current_distance;
      closest_ind = i;
    }
  }

  CHECK(closest_ind != -1);

  return closest_ind;
}

void HueRange::FitToRange(
    HueFitMethod fit_method,
    FloatImage* hsv_image) const {
  // TODO: test on multiple images and derive a good formula for lambda
  const float kLambda = 0.2;

  // In case we have a single segment, we split it into 2 segments and call
  // FitToRange() again (which assumes >1 segments)
  if (1 == hue_segments_.size()) {
    HueRange hue_range_split({hue_segments_[0].from_,
                              hue_segments_[0].Center(),
                              hue_segments_[0].Center(),
                              hue_segments_[0].to_}, true);
    hue_range_split.FitToRange(fit_method, hsv_image);
    return;
  }

  // At this point we know we have >1 segments

  // Init a 2D array that holds, for each pixel, an assignment into a segment.
  // We either choose the closest segment or use min-cut, depending on the
  // selected fit_method.
  Array2D<uint8> closest_segs;
  switch (fit_method) {
    case CLOSEST_VALID_VALUE:
    case LINEAR_SCALE:
    case GAUSSIAN_SCALE:
      AssignToSegmentUsingClosest(*hsv_image, &closest_segs);
      break;
    case LINEAR_SCALE_MINCUT:
      AssignToSegmentUsingMincut(*hsv_image, kLambda, &closest_segs);
      break;
    default:
      CHECK(false);
  }

  // For each segment, find the farthest pixel (which is mapped to that segment)
  // and store this "worst case" distance
  std::vector<float> max_distances(hue_segments_.size(), 0);
  for (int i = 0; i < hsv_image->width(); ++i) {
    for (int j = 0; j < hsv_image->height(); ++j) {
      const float hue_value = hsv_image->GetValue(i, j, 0);
      const int seg_index = closest_segs(j, i);
      const Segment seg = hue_segments_[seg_index];
      float dist_from_seg = 0;
      if (split_occurred_) {
        dist_from_seg = (seg_index == 0) ?
            seg.DistanceOneSidedBeforeEnd(hue_value) :
            seg.DistanceOneSidedAfterStart(hue_value);
      } else {
        dist_from_seg = seg.DistanceFromCenter(hue_value);
      }

      if (dist_from_seg > max_distances[seg_index]) {
        max_distances[seg_index] = dist_from_seg;
      }
    }
  }

  // If a segment was split, we need the same (maximal) max distance for both
  // parts
  if (split_occurred_) {
    max_distances[0] = max_distances[1] =
        std::max(max_distances[0], max_distances[1]);
  }

  // For each pixel, map it into its segment, scaling according to largest
  // distance to that segment
  for (int i = 0; i < hsv_image->width(); ++i) {
    for (int j = 0; j < hsv_image->height(); ++j) {
      const float old_hue_val = hsv_image->GetValue(i, j, 0);
      float new_hue_val;
      uint8 seg_index = closest_segs(j, i);
      const Segment seg = hue_segments_[seg_index];
      const float maximal_distance = max_distances[seg_index];

      // TODO: currently, if all the hue values are strictly within a hue
      // range, we will "stretch" the hue values to fill up the whole range.
      // Need to decide if this is the wanted behavior vs. just leaving the
      // pixels as is in such cases.

      switch (fit_method) {
        case CLOSEST_VALID_VALUE:
          if (split_occurred_) {
            new_hue_val = seg.FindClosestValueInSegment(
                old_hue_val,
                (seg_index == 0) ? PUSH_VALUES_FORWARD : PULL_VALUES_BACKWARD);
          } else {
            new_hue_val = seg.FindClosestValueInSegment(old_hue_val,
                                                        BOTH_SIDES);
          }
          break;
        case LINEAR_SCALE:
        case LINEAR_SCALE_MINCUT:
          if (split_occurred_) {
            new_hue_val = seg.FindLinearScaledValueInSegment(
                old_hue_val, maximal_distance,
                (seg_index == 0) ? PUSH_VALUES_FORWARD : PULL_VALUES_BACKWARD);
          } else {
            new_hue_val = seg.FindLinearScaledValueInSegment(
                old_hue_val, maximal_distance, BOTH_SIDES);
          }
          break;
        case GAUSSIAN_SCALE:
          if (split_occurred_) {
            new_hue_val = seg.FindGaussianScaledValueInSegment(
                old_hue_val, maximal_distance,
                (seg_index == 0) ? PUSH_VALUES_FORWARD : PULL_VALUES_BACKWARD);
          } else {
            new_hue_val = seg.FindGaussianScaledValueInSegment(
                            old_hue_val, maximal_distance, BOTH_SIDES);
          }
          break;
        default:
          CHECK(false);
      }

      hsv_image->SetValue(i, j, 0, new_hue_val);
    }
  }
}

int HueRange::NumberOfSegments() const {
  return hue_segments_.size();
}

void HueRange::FitHuesToRanges(
    const FloatImage& hsv_image,
    Array2D<uint8>* range_labels) const {
  // range_labels will hold a label for each pixel
  range_labels->Realloc(hsv_image.height(), hsv_image.width());
  for (int i = 0; i < range_labels->width(); ++i) {
    for (int j = 0; j < range_labels->height(); ++j) {
      (*range_labels)(j, i) = numeric_limits<uint8>::max();
    }
  }

  for (int i = 0; i < hsv_image.width(); ++i) {
    for (int j = 0; j < hsv_image.height(); ++j) {
      float current_hue = hsv_image.GetValue(i, j, 0);

      for (int k = 0; k < hue_segments_.size() - 1; ++k) {
        const Segment& seg = hue_segments_[k];
        const Segment& next_seg = hue_segments_[k + 1];
        if (seg.IsWithinSegmentOrBeforeOther(current_hue, next_seg)) {
          (*range_labels)(j, i) = k;
          break;
        }
      }
      if (numeric_limits<uint8>::max() == (*range_labels)(j, i)) {
        // if we've reached here, the value fits within the last range
        (*range_labels)(j, i) = hue_segments_.size() - 1;
      }
    }
  }
}

// A helper function to map pixels into two groups, according to two segments.
// We use the right boundary of the first segment and the left boundary of the
// right segment as our hue values, and use min-cut to segment.
void HueRange::LabelImageTwoSegments(Segment seg1,
    Segment seg2, const FloatImage& hsv_image,
    float lambda, Array2D<bool>* labels) {

  SimpleMaxFlow flow;

  // We index the pixels from 0 to numPixels-1. We index the source and sink
  // nodes as numPixels and numPixels+1, respectively.
  const NodeIndex source = hsv_image.height() * hsv_image.width();
  const NodeIndex sink = hsv_image.height() * hsv_image.width() + 1;

  const float source_hue = seg1.to_;
  const float sink_hue = seg2.from_;

  // We need to convert our float values to ints for the max_flow algorithm. We
  // do this by multiplying by a constant, selected as
  // max(long long) / 10^12. We chose this number since it is sufficiently
  // large, far enough from max(long long), which is used as the "infinity"
  // capacity value.
  static const int64 kMultFactor =
      std::numeric_limits<FlowQuantity>::max() / 1e12;

  // We use kMaxCapicity for sanity checks. We select our parameters such that
  // no edge capacity will ever exceed this value. This is enforced via CHECK().
  static const int64 kMaxCapicity = 1e5 * kMultFactor;

  // Create edges from source and to sink
  // Seg1 corresponds to source, seg2 corresponds to sink. We add an edge with
  // large capacity for pixels that are within the designated range, so that
  // these will act as a herd constraint (a pixel within a segment will always
  // belong to that segment.
  for (int i = 0; i < hsv_image.width(); ++i) {
    for (int j = 0; j < hsv_image.height(); ++j) {
      const float current_hue = hsv_image.GetValue(i, j, 0);
      const float current_saturation = hsv_image.GetValue(i, j, 1);
      const int linear_index = i * hsv_image.height() + j;

      // Calculate capacities of edges connecting to source/sink.
      // We set capacity to max if hue value is within the segment.

      FlowQuantity capacity_from_source;
      FlowQuantity capacity_to_sink;

      if (seg1.IsWithinSegment(current_hue)) {
        capacity_from_source = kMaxCapicity;
      } else {
        capacity_from_source = CircularDistance(source_hue, current_hue) *
            current_saturation * lambda * kMultFactor;
      }

      if (seg2.IsWithinSegment(current_hue)) {
        capacity_to_sink = kMaxCapicity;
      } else {
        capacity_to_sink = CircularDistance(sink_hue, current_hue) *
            current_saturation * lambda * kMultFactor;
      }

      CHECK(capacity_from_source <= kMaxCapicity);
      CHECK(capacity_to_sink <= kMaxCapicity);
      flow.AddArcWithCapacity(source, linear_index, capacity_from_source);
      flow.AddArcWithCapacity(linear_index, sink, capacity_to_sink);
    }
  }

  // Add edges between neighboring pixels (4-neighborhoods)
  for (int i = 0; i < hsv_image.width() - 1; ++i) {
    for (int j = 0; j < hsv_image.height() - 1; ++j) {
      // get values for current pixel
      const float current_hue = hsv_image.GetValue(i, j, 0);
      const float current_saturation = hsv_image.GetValue(i, j, 1);
      const int current_index = i * hsv_image.height() + j;

      // get values for right neighbor
      const float right_hue = hsv_image.GetValue(i + 1, j, 0);
      const float right_saturation = hsv_image.GetValue(i + 1, j, 1);
      const int right_index = (i + 1) * hsv_image.height() + j;

      // get values for bottom neighbor
      const float bottom_hue = hsv_image.GetValue(i, j + 1, 0);
      const float bottom_saturation = hsv_image.GetValue(i, j + 1, 1);
      const int bottom_index = i * hsv_image.height() + j + 1;

      // TODO: there might be a better value we can use here
      const float min_distance = 1e-3;

      // Calculate capacities of horizontal edges between neighboring pixels
      const FlowQuantity capacity_horizontal =
          kMultFactor * std::max(current_saturation, right_saturation) /
          CircularDistanceWithMinValue(current_hue, right_hue, min_distance);
      // Calculate capacities of vertical edges between neighboring pixels
      const FlowQuantity capacity_vertical =
          kMultFactor * std::max(current_saturation, bottom_saturation) /
          CircularDistanceWithMinValue(current_hue, bottom_hue, min_distance);

      CHECK(capacity_horizontal <= kMaxCapicity);
      CHECK(capacity_vertical <= kMaxCapicity);
      flow.AddArcWithCapacity(current_index, right_index, capacity_horizontal);
      flow.AddArcWithCapacity(right_index, current_index, capacity_horizontal);
      flow.AddArcWithCapacity(current_index, bottom_index, capacity_vertical);
      flow.AddArcWithCapacity(bottom_index, current_index, capacity_vertical);
    }
  }

  CHECK(flow.Solve(source, sink) == SimpleMaxFlow::OPTIMAL);

  // Return the linear indices of all pixels marked as belonging to seg1. The
  // rest belong to seg2.
  vector<NodeIndex> seg1_indices;
  flow.GetSinkSideMinCut(&seg1_indices);

  // Convert from a vector of indices to a 2D bool array
  LinearIndicesToBooleanArray(hsv_image.width(), hsv_image.height(),
                              seg1_indices, labels);
}

void HueRange::LinearIndicesToBooleanArray(
    int width, int height, const vector<NodeIndex>& indices,
    Array2D<bool>* bool_array) {
  bool_array->Realloc(height, width);
  for (int i = 0; i < bool_array->width(); ++i) {
    for (int j = 0; j < bool_array->height(); ++j) {
      (*bool_array)(j, i) = false;
    }
  }

  for (NodeIndex index : indices) {
    // We disregard out of bound indices
    if (index < height * width) {
      (*bool_array)(index % height, index / height) = true;
    }
  }
}

void HueRange::AssignToSegmentUsingClosest(
    const FloatImage& hsv_image,
    Array2D<uint8>* assignment) const {
  // Result will have the same height/width as our image
  assignment->Realloc(hsv_image.height(), hsv_image.width());

  // Find and store closest segment
  for (int i = 0; i < hsv_image.width(); ++i) {
    for (int j = 0; j < hsv_image.height(); ++j) {
      (*assignment)(j, i) =
          GetClosestSegmentIndices(hsv_image.GetValue(i, j, 0));
    }
  }
}

void HueRange::AssignToSegmentUsingMincut(
    const FloatImage&
    hsv_image, float lambda, Array2D<uint8>* assignment) const {

  // Will hold the returned value
  Array2D<uint8> labels;

  if (1 == hue_segments_.size()) {
    // If we only have a single hue range, the problem is much simpler. No need
    // to determine which two ranges are the candidate destinations for each hue
    // value.
    Array2D<bool> pixels_labels;
    LabelImageTwoSegments(hue_segments_[0], hue_segments_[0],
                          hsv_image, lambda, &pixels_labels);

    assignment->Realloc(hsv_image.height(), hsv_image.width());
    for (int i = 0; i < pixels_labels.width(); ++i) {
      for (int j = 0; j < pixels_labels.height(); ++j) {
        (*assignment)(j, i) = (pixels_labels(j, i)) ? 1 : 0;
      }
    }

    // No need to continue, we already got our assignment
    return;
  }

  // At this point we know we have >1 segments

  // We init the result array with MAX_UINT8. At the end of execution we expect
  // no such value in the array.
  assignment->Realloc(hsv_image.height(), hsv_image.width());
  for (int i = 0; i < assignment->width(); ++i) {
    for (int j = 0; j < assignment->height(); ++j) {
      (*assignment)(j, i) = numeric_limits<uint8>::max();
    }
  }

  // Determine, for each pixel, which are the candidate two segments
  Array2D<uint8> range_labels;
  FitHuesToRanges(hsv_image, &range_labels);

  for (int i = 0; i < hue_segments_.size(); ++i) {
    // compute a circular 'next_index'
    const int next_index = (i < hue_segments_.size() - 1) ? i + 1 : 0;

    // label according to the current and next segments
    Array2D<bool> pixels_labels;
    LabelImageTwoSegments(hue_segments_[i], hue_segments_[next_index],
                          hsv_image, lambda, &pixels_labels);

    // Iterate all pixels. If a pixel belongs to the current range, assign a
    // label
    for (int j = 0; j < range_labels.width(); ++j) {
      for (int k = 0; k < range_labels.height(); ++k) {
        if (i == range_labels(k, j)) {
          (*assignment)(k, j) = pixels_labels(k, j) ? i : next_index;
        }
      }
    }
  }
}

float HueRange::CircularDistance(float hue1, float hue2) {
  return std::min(std::abs(hue1 - hue2),
                  HueRange::kMaxHue - std::abs(hue1 - hue2));
}

float HueRange::CircularDistanceWithMinValue(float hue1, float hue2,
                                             float min_value) {
  return std::max(CircularDistance(hue1, hue2), min_value);
}

// The Segment subclass is used to represent a simple (circular) line segment

HueRange::Segment::Segment(float input_from, float input_to) :
        from_(input_from), to_(input_to) {
  float max_hue_val = HueRange::kMaxHue;

  CHECK(from_ >= 0);
  CHECK(to_ >= 0);
  CHECK(from_ < max_hue_val);
  CHECK(to_ < max_hue_val);
}

bool HueRange::Segment::IsWithinSegment(float value) const {
  // Test if a value is within a segment. Note that 2 is in [1,3] and 4 is
  // in [3,1].
  return ( (from_ <= to_ &&   (from_ <= value && value <= to_)) ||
           (from_ >  to_ &&   (from_ <= value || value <= to_)) );
}

bool HueRange::Segment::IsStrictlyWithinSegment(float value) const {
  // Test if a value is strictly within a segment. Note that 2 is strictly in
  // [1,3] but 1 and 3 are not.
  return ( (from_ <= to_ &&   (from_ < value && value < to_)) ||
           (from_ >  to_ &&   (from_ < value || value < to_)) );
}

bool HueRange::Segment::IsWithinSegmentOrBeforeOther(
    float value, Segment other) const {
  // Test if a value is within a segment or within the gap between the segment
  // and 'other'. For example, if the current segment is [1,2] and other is
  // [3,4] the the function will return true for 1, 1.5, 2, 2.5, 2.999 but false
  // for 3, 4, 6, 0, 0.999.

  return IsWithinSegment(value) ||
      (Segment(to_, other.from_).IsWithinSegment(value) &&
          value != other.from_);
}

// Notice that we only count a segment overlap, not a point overlap. Thus, the
// segments [1,3] and [2,4] overlap, but [1,2], [2,3] do not overlap
bool HueRange::Segment::IsOverlappingSegment(Segment other) const {
  return (  other.IsStrictlyWithinSegment(from_) ||
            other.IsStrictlyWithinSegment(to_) ||
            this->IsStrictlyWithinSegment(other.from_) ||
            this->IsStrictlyWithinSegment(other.to_) );
}

float HueRange::Segment::FindClosestValueInSegment(
    float value, SegmentScaleMethod scale_method) const {
  if (IsWithinSegment(value)) {
    return value;
  }

  switch (scale_method) {
    case BOTH_SIDES:
      // calculate result using distance between 'value' and 'from_' / 'to_'
      return (CircularDistance(from_, value) < CircularDistance(to_, value)) ?
             from_ : to_;
    case PULL_VALUES_BACKWARD:
      return to_;
    case PUSH_VALUES_FORWARD:
      return from_;
    default:
      CHECK(false);
  }
}

// TODO: move all the mapping functions into seperate classes with a
// generic interface. Given an arbitrary f() that maps into [-1, 1] or [0, 1],
// create a mapping.

float HueRange::Segment::FindLinearScaledValueInSegment(
    float value, float maximal_distance,
    SegmentScaleMethod scale_method) const {
  // We map from all distance values to the segment.
  // If scale_method is BOTH_SIDES, the new value will be
  // Center() + Width()/2 * f(), such that f() receives values in [-1,1].
  // This will cause the entire expression to be in
  // [Center() - Width()/2, Center() + Width()/2], which is exactly our segment.
  // Other mappings can be easily created by replacing f().
  // If scale_method is PULL_VALUES_BACKWARD, the new values will be
  // StartOfSeg() + Width() * g(), such that g() receives values in [0,1], again
  // mapping to the entire segment. A similar expression is used for
  // PUSH_VALUES_FORWARD.

  float retval = 0;
  switch (scale_method) {
    case BOTH_SIDES:
      retval = Center() + (Width() / 2) *
               (SignedDistanceFromCenter(value) / maximal_distance);
      break;
    case PULL_VALUES_BACKWARD:
      retval = from_ +
               (Width() * DistanceOneSidedAfterStart(value) / maximal_distance);
      break;
    case PUSH_VALUES_FORWARD:
      retval = to_ -
               (Width() * DistanceOneSidedBeforeEnd(value) / maximal_distance);
      break;
    default:
      CHECK(false);
  }

  // fix wrap around
  if (retval >= HueRange::kMaxHue) {
    retval -= HueRange::kMaxHue;
  } else if (retval < 0) {
    retval += HueRange::kMaxHue;
  }

  return retval;
}

double HueRange::Segment::Gaussian(double x, double sigma) {
  static const double sqrt_2_pi = 2.5066282746310002;

  return std::exp(-(x*x) / (2*sigma*sigma)) / (sigma * sqrt_2_pi);
}

double HueRange::Segment::NormalizedGaussian(double x, double sigma,
                                             float maximal_distance) {
  // Here we normalize such that (for maximal_distance == pi):
  // NormalizedGaussian(0) = 1
  // NormalizedGaussian(pi) = 0
  // NormalizedGaussian(-pi) = 0
  // And we don't care about values smaller than -maximal_distance or greater
  // than maximal_distance since they are not valid circular-distance values
  CHECK(x >= -maximal_distance);
  CHECK(x <= maximal_distance);
  return (Gaussian(x, sigma) - Gaussian(maximal_distance, sigma)) /
      (Gaussian(0, sigma) - Gaussian(maximal_distance, sigma));
}

float HueRange::Segment::FindGaussianScaledValueInSegment(
    float value, float maximal_distance,
    SegmentScaleMethod scale_method) const {
  float retval = 0;

  double dist = 0;
  double offset = 0;
  switch (scale_method) {
    case BOTH_SIDES:
      // signed_dist is in [-maximal_distance, maximal_distance]
      dist = SignedDistanceFromCenter(value);
      // offset is in [0, 1] such that 0 corresponds to value == center
      offset = 1 - NormalizedGaussian(dist, Width() / 2,
                                             maximal_distance);
      // add sign to offset (now it is in [-1, 1])
      if (dist < 0) {
        offset = -offset;
      }

      retval = Center() + (Width() / 2) * offset;
      break;
    case PULL_VALUES_BACKWARD:
      dist = DistanceOneSidedAfterStart(value);
      // offset is in [0,1] such that 0 corresponds to from_
      offset = 1 - NormalizedGaussian(dist, Width() / 2,
                                             maximal_distance);
      retval = from_ + (Width() * offset);
      break;
    case PUSH_VALUES_FORWARD:
      dist = DistanceOneSidedBeforeEnd(value);
      // offset is in [0,1] such that 0 corresponds to to_
      offset = 1 - NormalizedGaussian(dist, Width() / 2,
                                             maximal_distance);
      retval = to_ - (Width() * offset);
      break;
    default:
      CHECK(false);
  }

  // fix wrap around
  if (retval >= HueRange::kMaxHue) {
    retval -= HueRange::kMaxHue;
  } else if (retval < 0) {
    retval += HueRange::kMaxHue;
  }

  return retval;
}

float HueRange::Segment::DistanceFromEdge(float value) const {
  if (IsWithinSegment(value)) {
    return 0;
  }

  return DistanceFromCenter(value) - (Width() / 2);
}

float HueRange::Segment::DistanceFromCenter(float value) const {
  return std::abs(SignedDistanceFromCenter(value));
}

float HueRange::Segment::SignedDistanceFromCenter(float value) const {
  float max_hue_val = HueRange::kMaxHue;
  CHECK(value >= 0);
  CHECK(value < max_hue_val);

  float center = Center();

  // calculate distance such that 'value' follows 'center'
  float distance_after = value - center;
  if (distance_after < 0) {
    distance_after += HueRange::kMaxHue;
  }

  // calculate distance such that 'value' precedes 'center'
  float distance_before = center - value;
  if (distance_before < 0) {
    distance_before += HueRange::kMaxHue;
  }

  // Return a signed distance value. If we choose to place 'value' after the
  // segment, the returned distance is positive, otherwise negative. For example
  // distance of 5 from the center of [2,4] is 2, since the center is 3 and
  // 5-3 = 2. Notice that in the other direction we get a distance of 2pi - 2
  // which is larger than 2, which is why we chose to place 'value' after our
  // segment.
  if (distance_after < distance_before) {
    return distance_after;
  } else {
    return -distance_before;
  }
}

// Calculate a one-sided distance from the beginning of the segment onwards. For
// example, if the segment is [1,2], the distance of 0.3 will not be 0.7 (since
// that is the wrong direction). It will be 2*pi - 1 + 0.3, since we are
// measuring the distance to 'from_'
float HueRange::Segment::DistanceOneSidedAfterStart(float value) const {
  if (value < from_) {
    value += kMaxHue;
  }
  return value - from_;
}

// Calculate a one-sided distance to the end of the segment. For example, if the
// segment is [1,2], the distance of 0.3 will be 1.7 and the distance of 5 will
// be 2*pi - 5 + 2
float HueRange::Segment::DistanceOneSidedBeforeEnd(float value) const {
  if (value > to_) {
    value -= kMaxHue;
  }
  return to_ - value;
}

float HueRange::Segment::Center() const {
  float center = from_ + (Width() / 2);

  // fix if wrapped around
  if (center >= HueRange::kMaxHue) {
    center -= HueRange::kMaxHue;
  }

  return center;
}

float HueRange::Segment::Width() const {
  float width = to_ - from_;

  // fix for the case where from_ > to_
  if (width < 0) {
    width += HueRange::kMaxHue;
  }

  return width;
}

}  // namespace harmonizer
